#include "Frustum.hpp"
#include <array>

DirectX::XMVECTOR Vec(float x, float y, float z, float w)
{
    return DirectX::XMVectorSet(x, y, z, w);
}

using namespace DirectX;

BoundingFrustum::BoundingFrustum(DirectX::XMMATRIX proj)
{
    auto points = std::array {
        //top far plane (0, 1, 1, 1)
        Vec(0.0f, 1.0f, 1.0f, 1.0f),
        //left far plane (-1, 0, 1, 1)
        Vec(-1.0f, 0.0f, 1.0f, 1.0f),
        //bottom far plane (0, -1, 1, 1)
        Vec(0.0f, -1.0f, 1.0f, 1.0f),
        //right far plane
        Vec(1.0f, 0.0f, 1.0f, 1.0f),
        //center near plane
        Vec(0.0f, 0.0f, 0.0f, 1.0f),
        //center far plane
        Vec(0.0f, 0.0f, 1.0f, 1.0f)
    };

    enum : std::size_t {
        kTop = 0, kLeft, kBottom, kRight, kCenterNear, kCenterFar
    };

    const auto invProj = DirectX::XMMatrixInverse(nullptr, proj);
    for (auto& point : points)
    {
        point = DirectX::XMVector4Transform(point, invProj);
    }

    const auto GetRowSlope = [](DirectX::XMVECTOR vec) {
        return DirectX::XMVectorGetX(vec) / DirectX::XMVectorGetZ(vec);
    };

    const auto GetColSlope = [](DirectX::XMVECTOR vec) {
        return DirectX::XMVectorGetY(vec) / DirectX::XMVectorGetZ(vec);
    };

    const auto GetNearFar = [](DirectX::XMVECTOR vec) {
        return 1.0f / DirectX::XMVectorGetW(vec);
    };

    m_near = GetNearFar(points[kCenterNear]);
    m_far = GetNearFar(points[kCenterFar]);

    m_leftSlope = GetRowSlope(points[kLeft]);
    m_rightSlope = GetRowSlope(points[kRight]);
    m_topSlope = GetColSlope(points[kTop]);
    m_bottomSlope = GetColSlope(points[kBottom]);
}

BoundingFrustum BoundingFrustum::FromMatrix2(DirectX::XMMATRIX proj)
{
    const auto transposedProj = DirectX::XMMatrixTranspose(proj);
    BoundingFrustum frustum;
    const auto& row2 = transposedProj.r[2];
    frustum.m_near = -DirectX::XMVectorGetW(DirectX::XMPlaneNormalize(row2));
    frustum.m_far = -DirectX::XMVectorGetW(DirectX::XMPlaneNormalize(DirectX::XMVectorSubtract(transposedProj.r[2], transposedProj.r[3])));
    return frustum;
}

void BoundingFrustum::GenPlanes(std::array<DirectX::XMVECTOR, 6>& planes)
{
    planes[0] = XMPlaneNormalize(XMPlaneFromPointNormal(XMVectorSet(0.0f, 0.0f, m_near, 1.0f), g_XMIdentityR2));
    planes[1] = XMPlaneNormalize(XMPlaneFromPointNormal(XMVectorSet(0.0f, 0.0f, m_far, 1.0f), g_XMIdentityR2));
    //left
    planes[2] = XMPlaneNormalize(XMVectorSet(1.0f, 0.0f, m_leftSlope, 0.0f));
    //right
    planes[3] = XMPlaneNormalize(XMVectorSet(1.0, 0.0f, m_rightSlope, 0.0f));
    //top
    planes[4] = XMPlaneNormalize(XMVectorSet(0.0f, 1.0f, m_topSlope, 0.0f));
    //bottom
    planes[5] = XMPlaneNormalize(XMVectorSet(0.0f, 1.0f, m_bottomSlope, 0.0f));
}

BoundingFrustum::BoundingFrustum(float fovX, float aspect, float near, float far)
{
    m_near = near;
    m_far = far;
    const auto tanFov = std::tan(fovX / 2.0f);
    m_leftSlope = tanFov;
    m_rightSlope = -tanFov;
    const auto tanYFov = tanFov / aspect;
    m_topSlope = tanYFov;
    m_bottomSlope = -tanYFov;
}

void PlanesFromMatrix(DirectX::XMMATRIX projection, std::array<DirectX::XMVECTOR, 6>& planes)
{
    projection = DirectX::XMMatrixTranspose(projection);
    //near
    planes[0] = XMPlaneNormalize(projection.r[2]);
    //far
    planes[1] = XMPlaneNormalize(projection.r[2] - projection.r[3]);
    //left
    planes[2] = XMPlaneNormalize(projection.r[0] + projection.r[3]);
    //right
    planes[3] = XMPlaneNormalize(projection.r[0] - projection.r[3]);
    //top
    planes[4] = XMPlaneNormalize(projection.r[1] - projection.r[3]);
    //bottom
    planes[5] = XMPlaneNormalize(projection.r[1] + projection.r[3]);
}
